Skip to content

fix(server): name the process holding the port instead of a raw ServeError - #7703

Open
DraftProducts wants to merge 1 commit into
pingdotgg:mainfrom
DraftProducts:fix/server-port-in-use-message
Open

fix(server): name the process holding the port instead of a raw ServeError#7703
DraftProducts wants to merge 1 commit into
pingdotgg:mainfrom
DraftProducts:fix/server-port-in-use-message

Conversation

@DraftProducts

@DraftProducts DraftProducts commented Aug 20, 2026

Copy link
Copy Markdown

What Changed

A bind failure carrying EADDRINUSE now fails as PortInUseError, which names
the port and, when server-runtime.json corroborates it, the process holding it.
Every other bind failure passes through untouched, and the original ServeError
and errno error stay attached as cause, so nothing is lost for debugging.

Corroborated:

Port 3773 on 127.0.0.1 is already in use by a running T3 Code server (pid
1080 per server-runtime.json). Stop that server first; 't3 service status'
finds it when it is the background service. If that pid is not actually a T3
server (stale descriptor, reused pid), delete '<path>' and retry.

When the state file is missing or names a different port, no culprit is invented:

Port 3773 on 127.0.0.1 is already in use. Stop whatever is listening there,
or start T3 Code on another port with --port; 't3 service status' says
whether the T3 Code background service is holding it.

holderPid is set only when the persisted state parses and its port matches the
one being bound. That is the same shape — and the same hedge about stale
descriptors and reused pids — that migrate-dev-db.ts already uses for a
database held open by a running server.

explainPortInUse wraps runServer rather than HttpServerLive. Wrapping the
layer is the more precise spot, but Layer.catchCause types the recovery as
Layer<ROut & ROut2> and a failing fallback layer has ROut2 = never, which
collapses the HttpServer output to never. Layer.launch is the first point
where the failure is an ordinary Effect error again, and ServerConfig is
already in its requirement channel, so no new requirement reaches bin.ts. The
predicate walks the cause chain for code === "EADDRINUSE" across both Fail
and Die, since @effect/platform-node fails the layer build with a ServeError
while BunHttpServer throws the errno error as a defect. Catching at runServer
is marginally broader than the bind alone; the only other server built during
startup is in the CLI auth flow, and the EADDRINUSE predicate keeps an unrelated
failure from being relabeled.

Tests: five cases in portInUse.test.ts, three of which build a real
NodeHttpServer.layer against a port held by a live net.Server, so the failure
under test is a genuine EADDRINUSE rather than a synthetic cause. They assert the
full corroborated message, the full degraded message, that a state file naming
another port does not produce a pid, that a Bun-shaped defect is still explained,
and that a ServeError wrapping EACCES comes through identical. Stubbing
explainPortInUse to identity fails four of the five. server.test.ts (124) and
bin.test.ts (17) pass, as do apps/server typecheck, lint and format. No
pre-existing failures on this path.

Why

ServeError names nothing. A user running the background service in WSL, with
the desktop app launching its own backend into the same distro on the same port,
got only this — in a log file they had to know to look for:

ERROR (#5): ServeError:
    at Server.onError (.../@effect/platform-node/dist/NodeHttpServer.js:74:26)
  [cause]: Error: listen EADDRINUSE: address already in use 0.0.0.0:3773

The server knows which port it was told to bind, and server-runtime.json is
already read by t3 triage and t3 pair to find a live server, so the answer to
"who has it" is one file read away at exactly the moment it matters.

The message names the service explicitly because that is the common way to end up
with a T3 Code server you forgot about, but it has to stay correct for a plain
npx t3 on a busy port, which is why the degraded variant claims nothing it
cannot support.

Not addressed: the pid is not liveness-checked. triage.ts and
migrate-dev-db.ts both process.kill(pid, 0) before claiming a server runs, so
there is precedent, but a second heuristic with its own false positives did not
earn its place next to a message that already hedges. Separately,
cli/config.ts:262-265 returns DEFAULT_PORT with no availability check in
desktop mode where web mode calls findAvailablePort — that is why the
reported user hit this on every relaunch, and it is a different fix.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — N/A, server-only
  • I included a video for animation/interaction changes — N/A, server-only

Changes by Claude Opus 5 running in Claude Code.


Note

Low Risk
Error-path only: remaps EADDRINUSE after bind fails and does not change listen, auth, or persistence. Holder pid is not liveness-checked, which the message already hedges.

Overview
When the HTTP server fails to bind with EADDRINUSE, startup now fails as PortInUseError instead of a raw ServeError. The message names the port and, if server-runtime.json lists the same port, the recorded pid—plus how to stop the background service or drop a stale descriptor. Other bind failures are unchanged.

explainPortInUse wraps runServer after Layer.launch so Node ServeError and Bun defects both get rewritten without collapsing the HttpServer layer type. Tests occupy a real loopback port and cover corroborated pid, missing/mismatched state, Bun-shaped defects, and non-EADDRINUSE errors.

Reviewed by Cursor Bugbot for commit 5d78054. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Replace raw ServeError with PortInUseError naming the process holding the port

  • Introduces PortInUseError in portInUse.ts, carrying host, port, optional holderPid, and a human-readable message with remediation steps when a pid is known.
  • Adds explainPortInUse, an effect transformer that detects EADDRINUSE through up to three nested causes, reads server-runtime.json, and fails with PortInUseError including the persisted pid when the port matches.
  • Wraps Layer.launch(makeServerLayer) in server.ts with explainPortInUse so server startup surfaces the holder pid instead of a raw platform error.
  • Behavioral Change: non-EADDRINUSE bind failures (e.g. EACCES) pass through unchanged; the pid is only included when server-runtime.json exists and refers to the same port.

Macroscope summarized 5d78054.

…Error

When the HTTP server could not bind, startup failed with a bare `ServeError`
whose only detail was a stack frame in @effect/platform-node. A user whose
background service already held 127.0.0.1:3773 saw the desktop backend die
every 20s with nothing naming the port or the culprit.

EADDRINUSE now fails with a `PortInUseError` that names the port and host,
and names the holding pid when server-runtime.json describes that same port.
Without that corroboration it names only the port and points at
`t3 service status` rather than inventing a culprit. The original ServeError
is kept as the cause, and every other bind failure passes through untouched.

Model: claude-opus-5. Harness: Claude Code.
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e8287353-93df-4f7a-b70e-b213f2a5e325

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 20, 2026
@macroscopeapp

macroscopeapp Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 5d78054

Macroscope's review found this PR approvable — This PR improves error messaging when port binding fails, providing actionable guidance (including which process may hold the port) instead of raw EADDRINUSE errors. Changes are self-contained to error paths with comprehensive test coverage and no impact to normal server operation.

You can add or adjust custom eligibility rules. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant